let _Vue = null
export default class VueRouter {
  /**
   * 作用：实现Vue插件机制
   * install是一个静态方法
   * install接受一个Vue构造函数的参数和一个可选的选项参数
   * install中需要先判断VueRouter是否已经被安装，如果已经安装了就不需要重复安装
   * 需要将Vue的构造函数记录到全局变量中，因为VueRouter的一些实例方法中可能会需要使用到Vue的构造函数
   * 将创建Vue实例时传入的router对象给注入到所有的Vue实例上（Vue的所有Vue页面\组件都是Vue实例）
   */
  static install (Vue) {
    /**
     * 1.判断VueRouter是否已经被安装
     * 设置一个install的内置属性[installed]用于判断install是否已经被执行
     * 2.如果没有被安装则先将该插件标记为已安装
     **/
    if (this.install.installed) return
    this.install.installed = true

    // 2.把Vue构造函数记录到全局变量
    _Vue = Vue

    /**
     * 3. 将创建Vue实例时传入的router对象给注入到所有的Vue实例上
     * 我们想要让所有Vue实例共享一个成员，我们可以在Vue的构造函数的原型上添加，Vue实例会将Vue Router的实例router添加到$options中
     * _Vue.prototype.$router = this.$options.router
     * 按照上面的写法可以将传入的router注入到_Vue的原型上，但是此时this指向的是VueRouter类并不是Vue实例（install是VueRouter的静态方法，install的调用是VueRouter.install因此this指向的是VueRouter）
     * 而我们想要将创建Vue实例时传入的router对象给注入，就需要在Vue实例已经创建的情况下去将传入的router进行赋值
     * 因此此处我们可以使用到混入mixin，在Vue实例的生命周期创建前去注入，此时的this指向的就是当前的Vue实例对象了
     * 但我们给Vue原型挂载这个$router只需要在第一次挂在就可以了不需要每一个Vue的页面和Vue的实例创建时都进行挂载，因此需要判断创建Vue实例的时是否有传入router
     **/
    _Vue.mixin({
      beforeCreate () {
        if (this.$options.router) {
          _Vue.prototype.$router = this.$options.router
          // 将router实例注入到Vue实例以后初始化VueRouter
          this.$options.router.init()
        }
      }
    })
  }

  /**
   * 作用：初始化VueRouter的属性
   * 接受一个参数，参数是一个对象
   * 返回一个VueRouter对象
   * 构造函数需要初始化Vue Router属性
   * @author chaiyf
   * @date 2020-12-15 14:22
   */
  constructor (options) {
    this.options = options
    // 设置使用的模式（默认hash）
    this.mode = options.mode || 'hash'
    this.routeMap = {}
    // 使用Vue提供的observable函数将传入对象变成响应式对象
    this.data = _Vue.observable({
      current: '/' // 用来记录当前路由地址，默认为/
    })
  }

  /**
   * 作用：整合需要初始化的方法方便调用
   * @author chaiyf
   * @date 2020-12-16 20:11
   */
  init () {
    this.createRouteMap()
    this.initComponents(_Vue)
    this.initEvent()
    if (this.mode === 'hash') this.initHash()
  }

  /**
   * 作用：将构造函数中传入的路由规则，转换成健值对的形式存储到routerMap中，routerMap中存储的键就是路由地址，值就是该地址对应的组件
   * 遍历所有的路由规则，把路由规则转换成键值对的形式存储到routeMap中
   * @author chaiyf
   * @date 2020-12-15 15:09
   */
  createRouteMap () {
    this.options.routes.forEach(route => {
      this.routeMap[route.path] = route.component
    })
  }

  /**
   * 创建<router-link>和<router-view>组件
   * @author chaiyf
   * @date 2020-12-16 19:59
   * @param {object} Vue Vue构造函数
   */
  initComponents (Vue) {
    const self = this
    /**
     * 实现router-link组件
     * 1.首先创建一个组件要使用Vue.component
     * 2.router-link组件需要一个String类型的参数to
     * 3.router-link最终渲染出来是一个a标签，因此使用render函数去创建一个a元素，并设置属性href为props中的to属性
     * 4.标签中的内容是变化的，因此需要为a元素的子元素添加一个默认的插槽元素this.$slots.default
     * 5.为了我们点击时不向服务器发送请求，只是地址栏的url变化，并且页面随着变化渲染，因此我们需要阻止并重写a标签的点击事件
     */
    Vue.component('router-link', {
      props: {
        to: String
      },
      render (h) {
        /**
         * h函数（来源于Vue）
         * @author chaiyf
         * @date 2020-12-16 20:30
         * @param {string} 需要创建的元素选择器
         * @param {object} 元素的属性
         * @param {array} 元素的子元素
         */
        return h('a', {
          attrs: {
            href: this.mode === 'history' ? this.to : `${location.href}/#/${this.to}}`
          },
          // 此处为a标签定义一个点击事件
          on: {
            click: this.clickHandle
          }
        }, [this.$slots.default])
      },
      methods: {
        /**
         * 阻止并重写a标签的点击事件
         * 1.阻止浏览器的默认事件
         * 2.history模式下，使用history.pushState改变浏览器的url
         * 3.hash模式下，使用window.location.hash改变浏览器的url
         * 3.找到对应的页面组件并渲染
         */
        clickHandle (e) {
          if (this.mode === 'history') {
            /**
             * history.pushState有三个参数
             * 1.data: 该参数是为了实现页面的prop参数传递
             * 2.title: 网页的标题
             * 3.url: 当前超链接要跳转的地址
             */
            history.pushState({}, '', this.to)
            /**
             * 此处我们改变Vue Route实例的当前的路由属性，因为该属性为响应式的，因此会触发router-view的页面渲染
             * 此处的点击实在Vue Router实例创建以后，因此此时通过Vue Router的install()函数，我们已经将Vue Router的实例添加到了Vue实例的原型上了
             * 所以此时我们可以通过this.$router访问Vue Router的实例，从而访问到Vue Router实例的current属性
             */
            this.$router.data.current = this.to
          } else {
            window.location.hash = this.to
          }
          e.preventDefault()
        }
      }
    })

    /**
     * 实现router-view组件
     * 1、先要找到当前路由的地址（Vue Router实例的data 属性中的 current属性记录的就是当前路由地址）
     * 2、根据当前路由地址从routerMap中找到对应的组件
     * 3、然后利用h函数将我们找到的这个虚拟DOM
     */
    Vue.component('router-view', {
      render (h) {
        /**
         * 根据当前路由地址找到对应的组件
         * 因为此处的this指向render函数，而我们需要Vue Router实例，因此需要先在外部存储实例的this
         */
        const component = self.routeMap[self.data.current]
        return h(component)
      }
    })
  }

  /**
   * 实现点击浏览器返回、前进时url改变触发页面模版改变
   * 为我们的应用添加popstate事件，当url发生改变触发页面渲染（pushState、replaceState不会触发popstate事件）
   * popstate事件：当活动历史记录条目更改时，将触发popstate事件。
   */
  initEvent () {
    if (this.mode === 'history') {
      window.addEventListener('popstate', () => {
        // 当触发popstate事件时改变Vue Router实例的当前页面属性，从而触发模版的改变渲染
        this.data.current = window.location.pathname
      })
    } else {
      window.addEventListener('hashchange', () => {
        // 当触发hashchange事件时改变Vue Router实例的当前页面属性，从而触发模版的改变渲染
        this.data.current = window.location.hash.substr(1)
      })
    }
  }

  /**
   * hash 模式下，创建VueRouter实例时根据当前 url 的 hash 初始化页面
   */
  initHash () {
    const hash = window.location.hash.substr(1)
    if (hash === this.data.current) return
    this.data.current = hash
  }
}
